#region MIT License
//Copyright (c) 2007 Shane Isbell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy 
// of this software and associated documentation files (the "Software"), to 
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is 
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
// THE SOFTWARE. 
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Archivist.Tar
{
    public sealed class TarFile 
    {
        private List<TarEntry> tarEntries;

        private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1);

        /// <summary>
        /// Constructor. 
        /// </summary>
        /// <param name="fileInfo">The tar file to read from.</param>
        public TarFile(FileInfo fileInfo) 
        {
            if (fileInfo == null)
            {
                throw new ArgumentNullException("fileInfo", "ARC-000-000: File info is null");
            }
            new TarFile(new FileStream(fileInfo.FullName, FileMode.Open));
        }

        public TarFile(ICollection<FileSystemInfo> files, DirectoryInfo baseDirectory)
        {
            tarEntries = new List<TarEntry>();
            foreach (FileSystemInfo fsi in files)
            {
                if (fsi is DirectoryInfo)
                {
                    tarEntries.AddRange(CreateTarEntriesFromDirectory(baseDirectory,
                        (DirectoryInfo)fsi));
                }
                else
                {
                    tarEntries.Add(CreateTarEntryFromFile(baseDirectory, fsi));
                }
            }              
        }

        public TarFile(DirectoryInfo directoryInfo)
        {
            tarEntries = new List<TarEntry>();
            tarEntries.AddRange(CreateTarEntriesFromDirectory(directoryInfo.Parent, directoryInfo));
        }

        private static List<TarEntry> CreateTarEntriesFromDirectory(DirectoryInfo baseDirectory, 
            DirectoryInfo currentDirectory)
        {
            List<TarEntry> entries = new List<TarEntry>();
            entries.Add(CreateTarEntryFromFile(baseDirectory, currentDirectory));
            if (currentDirectory.GetFiles() != null)
            {
                foreach (FileInfo fileInfo in currentDirectory.GetFiles())
                {
                    Console.WriteLine("Create file: {0}", fileInfo.FullName);
                    entries.Add(CreateTarEntryFromFile(baseDirectory, fileInfo));
                }
            }

            if (currentDirectory.GetDirectories() != null)
            {
                foreach (DirectoryInfo dir in currentDirectory.GetDirectories())
                {
                    entries.AddRange(CreateTarEntriesFromDirectory(baseDirectory, dir));
                }
            }
            return entries;
        }

        private static TarEntry CreateTarEntryFromFile(DirectoryInfo baseDirectory, 
            FileSystemInfo tarFileInfo)
        {
            TarEntry tarEntry = new TarEntry();
            tarEntry.FileName = tarFileInfo.FullName.Substring(baseDirectory.FullName.Length + 1);

            tarEntry.UserId = "0";
            tarEntry.GroupId = "0";
            tarEntry.OwnerAccessRights = AccessRights.Read_Write;
            tarEntry.OtherAccessRights = AccessRights.Read_Write;
            tarEntry.GroupAccessRights = AccessRights.Read_Write;
            tarEntry.ModificationTime = tarFileInfo.LastWriteTimeUtc;

            if (tarFileInfo is DirectoryInfo)
            {
                tarEntry.FileType = FileType.Directory;
                tarEntry.FileTypeForFileMode = FileTypeForFileMode.Directory;
            }
            else
            {
                FileStream fileStream = new FileStream(tarFileInfo.FullName, FileMode.Open);
                try
                {
                    byte[] buffer = new byte[fileStream.Length];
                    fileStream.Read(buffer, 0, (int)fileStream.Length);//TODO: Could truncate data
                    tarEntry.Data = buffer;
                    tarEntry.Length = buffer.Length;
                }
                finally
                {
                    fileStream.Close();
                }
                tarEntry.FileType = FileType.Normal_File;
                tarEntry.FileTypeForFileMode = FileTypeForFileMode.Regular_File;
                long mod = tarEntry.Length % 512;
                tarEntry.PaddedLength = tarEntry.Length + ((mod == 0) ? 0 : (512 - mod));
            }
            return tarEntry;
        }

        //private static List<FileInfo>

        /// <summary>
        /// Constructor. This method is responsible for closing the specified input stream.
        /// </summary>
        /// <param name="inputStream">The tar input stream to read from.</param>
        public TarFile(Stream inputStream)
        {
            if (inputStream == null)
            {
                throw new ArgumentNullException("inputStream", "ARC-000-001: Input stream is null");
            }

            if (!inputStream.CanRead)
            {
                throw new IOException("ARC-000-002: Cannot read the stream.");
            }

            if (inputStream is FileStream && inputStream.Length <= 0)
            {
                throw new IOException("ARC-000-003: Stream is empty");
            }
 
            tarEntries = new List<TarEntry>();
            try
            {
                while (true)
                {
                    int eof = 0;
                    try
                    {
                        TarEntry tarEntry = new TarEntry();
                        eof = ReadTarEntry(inputStream, tarEntry);

                        if (tarEntry.FileName.Contains("LongLink"))
                        {
                            TarEntry tarEntryLongLink = new TarEntry();
                            eof = ReadTarEntry(inputStream, tarEntryLongLink);
                            tarEntryLongLink.FileName = GetBytesAsAsciiString(tarEntry.Data, "data");
                            tarEntries.Add(tarEntryLongLink);
                        }
                        else
                        {
                            tarEntries.Add(tarEntry);
                        }
                    }
                    catch (Exception e)
                    {
                        if (inputStream is FileStream)
                        {
                            Console.WriteLine("Skipping unreadable block: Range = {0}-{1}, Message = {2}",
                                inputStream.Position, inputStream.Position + 512, e.Message);
                        }
                        else
                        {
                            Console.WriteLine("Skipping unreadable block: Message = {0}", e.Message);
                        }
                    }
                    if (eof <= 0)
                    {
                        
                        break;
                    }
                }
            }
            finally
            {
                inputStream.Close();
            }
        }

        /// <summary>
        /// Default Constructor
        /// </summary>
        public TarFile()
        {
            tarEntries = new List<TarEntry>();
        }

        /// <summary>
        /// Adds the specified tar entry to the tar file.
        /// </summary>
        /// <param name="tarEntry">Entry to add to the tar file.</param>
        public void AddTarEntry(TarEntry tarEntry)
        {
            if (tarEntry == null)
            {
                throw new ArgumentNullException("tarEntry");
            }
            ICollection<TarEntryExceptions> exceptions = VerifyTarEntry(tarEntry);
            if (exceptions.Count > 0)
            {
                throw new ArgumentException("The tar entry contains invalid data.", "tarEntry");
            }
            tarEntries.Add(tarEntry);
        }

        /// <summary>
        /// Adds the specified collection of tar file entries to the tar file.
        /// </summary>
        /// <param name="newTarEntries">Entries to add to the tar file.</param>
        public void AddTarEntries(ICollection<TarEntry> newTarEntries)
        {
            if (newTarEntries == null)
            {
                throw new ArgumentNullException("newTarEntries");
            }
            //verify entries
            tarEntries.AddRange(newTarEntries);
        }

        /// <summary>
        /// Returns a live collection of the tar entries.
        /// </summary>
        /// <returns></returns>
        public ICollection<TarEntry> GetTarEntries()
        {
            return tarEntries;
        }

        public void WriteTarFileToStream(Stream outputStream)
        {
            WriteTarFileToStream(outputStream, false);
        }

        public void WriteTarFileToStream(Stream outputStream, bool leaveOpen)
        {
            if (outputStream == null)
            {
                throw new ArgumentNullException("outputStream");
            }

            try
            {
                foreach (TarEntry tarEntry in tarEntries)
                {
                    Console.WriteLine(tarEntry.FileName);
                    WriteTarEntry(outputStream, tarEntry);
                }
            }
            finally
            {
                outputStream.WriteByte(0);
                outputStream.WriteByte(0);
                outputStream.Write(new byte[1024], 0, 1024);
                if (!leaveOpen)
                {
                    outputStream.Close();
                }
            }
        }

        #region Internal Methods
        internal static void WriteFileModeToStream(Stream stream, TarEntry tarEntry)
        {
            byte[] fileMode = new byte[8];
            FileTypeForFileMode type = tarEntry.FileTypeForFileMode;
            if (type.Equals(FileTypeForFileMode.Regular_File))
            {
                fileMode[0] = 49;
                fileMode[1] = 48;
            }
            else if (type.Equals(FileTypeForFileMode.Symbolic_Link))
            {
                fileMode[0] = 49;
                fileMode[1] = 50;
            }
            else if (type.Equals(FileTypeForFileMode.Socket))
            {
                fileMode[0] = 49;
                fileMode[1] = 52;
            }
            else if (type.Equals(FileTypeForFileMode.Fifo))
            {
                fileMode[0] = 32;
                fileMode[1] = 49;
            }
            else if (type.Equals(FileTypeForFileMode.Character_Device))
            {
                fileMode[0] = 32;
                fileMode[1] = 50;
            }
            else if (type.Equals(FileTypeForFileMode.Directory))
            {
                fileMode[0] = 32;
                fileMode[1] = 52;
            }
            else if (type.Equals(FileTypeForFileMode.Block_Device))
            {
                fileMode[0] = 32;
                fileMode[1] = 54;
            }
            else
            {
                fileMode[0] = 32;
                fileMode[1] = 32;
            }
            fileMode[2] = 48;//TODO: Special Access Rights
            fileMode[3] = (byte)tarEntry.GroupAccessRights;
            fileMode[4] = (byte)tarEntry.OwnerAccessRights;
            fileMode[5] = (byte)tarEntry.OtherAccessRights;
            fileMode[6] = 32;
            stream.Write(fileMode, 0, 8);
        }

        internal static long GetFileLength(byte[] length)
        {
            if (length == null)
            {
                throw new ArgumentNullException("length", "ARC-000-009: File length is null");
            }

            if (length.Length == 0)
            {
                throw new ArgumentException("ARC-000-012: Length field is empty.");
            }

            if (!IsNumber(length))
            {
                throw new ArgumentException("ARC-000-011: Length field contain non-numeric characters.");
            }
            long filelength = Convert.ToInt64(Encoding.ASCII.GetString(length).Trim(), 8);
            return filelength;
        }

        internal static String GetBytesAsAsciiString(byte[] value, String name)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value", "ARC-000-004: Name = " + name);
            }

            if (!IsAscii(value))
            {
                throw new ArgumentException("ARC-000-006: Non-ascii value. Name = " + name);
            }

            String valueAsString = Encoding.ASCII.GetString(value).Trim('\0').Trim();
            if (String.IsNullOrEmpty(valueAsString))
            {
                throw new ArgumentException("ARC-000-005: Value is empty. Name = " + name);
            }

            return valueAsString;
        }

        internal int ReadTarEntry(Stream stream, TarEntry tarEntry)
        {
            byte[] fileName = new byte[100];
            byte[] fileMode = new byte[8];
            byte[] ownerId = new byte[8];
            byte[] groupId = new byte[8];
            byte[] fileLength = new byte[12];
            byte[] modTime = new byte[12];
            byte[] checkSum = new byte[8];
            byte[] fileType = new byte[1];
            byte[] nameOfLinkedFile = new byte[100];
            byte[] extendedData = new byte[255];

            int eof = 0;

            eof = stream.Read(fileName, 0, 100);
            eof = stream.Read(fileMode, 0, 8);
            eof = stream.Read(ownerId, 0, 8);
            eof = stream.Read(groupId, 0, 8);
            eof = stream.Read(fileLength, 0, 12);
            eof = stream.Read(modTime, 0, 12);
            eof = stream.Read(checkSum, 0, 8);
            eof = stream.Read(fileType, 0, 1);
            eof = stream.Read(nameOfLinkedFile, 0, 100);
            eof = stream.Read(extendedData, 0, 255);
            
            //File Name
            tarEntry.FileName = GetBytesAsAsciiString(fileName, "fileName");

            //File Mode
            try
            {
                tarEntry.FileTypeForFileMode = GetFileTypeForFileMode(fileMode[0], fileMode[1]);
                tarEntry.SpecialAccessRights = GetSpecialAccessRightsFor(fileMode[2]);
                tarEntry.GroupAccessRights = GetAccessRightsFor(fileMode[3]);
                tarEntry.OwnerAccessRights = GetAccessRightsFor(fileMode[4]);
                tarEntry.OtherAccessRights = GetAccessRightsFor(fileMode[5]);                
            }
            catch (IOException e)
            {
                Console.WriteLine(e.Message + ":" + tarEntry.FileName);
            }
     
            //Owner Id
            tarEntry.UserId = GetBytesAsAsciiString(ownerId, "ownerId");

            //Group Id
            tarEntry.GroupId = GetBytesAsAsciiString(groupId, "groupId");

            //File Length
            tarEntry.Length = GetFileLength(fileLength);
            long mod = tarEntry.Length % 512;
            tarEntry.PaddedLength = tarEntry.Length + ((mod == 0) ? 0 : (512 - mod));

            //Modification Time           
            tarEntry.ModificationTime = UnixEpoch.AddSeconds(
                Convert.ToInt64(Encoding.ASCII.GetString(modTime).Trim(), 8));
            
            //Checksum
            tarEntry.Checksum = Convert.ToInt64(Encoding.ASCII.GetString(checkSum).Trim().Trim('\0'), 8);

            //File Type
            tarEntry.FileType = GetFileTypeFor(fileType[0]);
            
            //Data
            if (tarEntry.Length != 0)
            {
                byte[] data = new byte[tarEntry.PaddedLength];
                eof = stream.Read(data, 0, (int)tarEntry.PaddedLength);
                tarEntry.Data = data;
            }
            else
            {
                tarEntry.Data = new byte[0];
            }

            return eof;
        }

        internal static void WriteFileNameToStream(Stream stream, String fileName)
        {
            byte[] fn = new byte[100];
            if (fileName.Length < 100)
            {
                Encoding.ASCII.GetBytes(fileName).CopyTo(fn, 0);
            }
            else
            {
                Encoding.ASCII.GetBytes(fileName.Substring(0, 99)).CopyTo(fn, 0);
            }
            stream.Write(fn, 0, 100);
        }
        #endregion

        #region Private Methods
        private static void CopyFromDecimalToOctal(int srcValue, byte[] destArray)
        {           
            Encoding.ASCII.GetBytes(Convert.ToString(srcValue, 8).PadLeft(11).PadRight(12)).CopyTo(destArray, 0);
        }

        private static void WriteIdToStream(Stream stream, String id)
        {
            byte[] x = new byte[8];
            if (id.Length > 7)
            {
                throw new ArgumentException("The owner or group id is more than 7 characters: ID = " + id);
            }
            Encoding.UTF8.GetBytes(id.PadLeft(6).PadRight(7)).CopyTo(x, 0);
            stream.Write(x, 0, 8);
        }

        private void WriteTarEntry(Stream stream, TarEntry tarEntry)
        {
            Stream memoryStream = new MemoryStream();
            if (tarEntry.FileName.Length >= 100 && !tarEntry.FileName.Contains("LongLink"))
            {
                TarEntry longLinkEntry = new TarEntry();
                longLinkEntry.FileName = @"././@LongLink";
                longLinkEntry.FileType = tarEntry.FileType;
                longLinkEntry.FileTypeForFileMode = tarEntry.FileTypeForFileMode;
                longLinkEntry.GroupAccessRights = tarEntry.GroupAccessRights;
                longLinkEntry.GroupId = tarEntry.GroupId;
                longLinkEntry.ModificationTime = DateTime.Now;
                longLinkEntry.OtherAccessRights = tarEntry.OtherAccessRights;
                longLinkEntry.OwnerAccessRights = tarEntry.OwnerAccessRights;
                longLinkEntry.SpecialAccessRights = tarEntry.SpecialAccessRights;
                longLinkEntry.UserId = tarEntry.UserId;
                longLinkEntry.Length = tarEntry.FileName.Length;
                longLinkEntry.FileType = FileType.Long_Link;
                long mod = longLinkEntry.Length % 512;
                longLinkEntry.PaddedLength = longLinkEntry.Length + ((mod == 0) ? 0 : (512 - mod));

                longLinkEntry.Data = Encoding.ASCII.GetBytes(tarEntry.FileName);
                WriteTarEntry(stream, longLinkEntry);
            }
            
            WriteFileNameToStream(memoryStream, tarEntry.FileName);
            WriteFileModeToStream(memoryStream, tarEntry);
            WriteIdToStream(memoryStream, tarEntry.GroupId);
            WriteIdToStream(memoryStream, tarEntry.UserId);
 
            byte[] fileLength = new byte[12];
            CopyFromDecimalToOctal((int)tarEntry.Length, fileLength);
            memoryStream.Write(fileLength, 0, 12);

            byte[] modTime = new byte[12];
            TimeSpan timeSpan = tarEntry.ModificationTime - UnixEpoch;
            long timeSinceUnixEpoch = ((((((timeSpan.Days * 24) + timeSpan.Hours) * 60) + timeSpan.Minutes) * 60) 
                + timeSpan.Seconds);
            CopyFromDecimalToOctal((int) timeSinceUnixEpoch, modTime);
            memoryStream.Write(modTime, 0, 12);

            byte[] checkSum = { 32, 32, 32, 32, 32, 32, 32, 32 };
            memoryStream.Write(checkSum, 0, 8);

            byte[] fileType = new byte[1];
            fileType[0] = (byte)tarEntry.FileType;          
            memoryStream.Write(fileType, 0, 1);

            byte[] nameOfLinkedFile = new byte[100];
            if (!String.IsNullOrEmpty(tarEntry.NameOfLinkedFile))
            {
                Encoding.ASCII.GetBytes(tarEntry.NameOfLinkedFile).CopyTo(nameOfLinkedFile, 0);
            }
            memoryStream.Write(nameOfLinkedFile, 0, 100);
            
            //Checksum
            byte[] buffer = new byte[memoryStream.Length];
            int cs = 0;
            memoryStream.Position = 0;
            for (int i = 0; i < buffer.Length; i++)
            {
                int b = memoryStream.ReadByte();
                buffer[i] = (byte) b;
                cs += b;
            }

            Encoding.ASCII.GetBytes(Convert.ToString(cs, 8).PadLeft(6)).CopyTo(checkSum, 0);
            checkSum[6] = 0;
            checkSum[7] = 32;
            checkSum.CopyTo(buffer, 148);

            stream.Write(buffer, 0, buffer.Length);

            //TODO: Add support for ustar
            byte[] extendedData = new byte[255];
            stream.Write(extendedData, 0, 255);

            if (tarEntry.Data != null)
            {               
                int nulledByteCount = (int) (tarEntry.PaddedLength - tarEntry.Length);
                stream.Write(tarEntry.Data, 0, (int) tarEntry.Length);
                stream.Write(new byte[nulledByteCount], 0, nulledByteCount);
            }
            
            stream.Flush();
        }

        private ICollection<TarEntryExceptions> VerifyTarEntry(TarEntry tarEntry)
        {
            ICollection<TarEntryExceptions> exceptions = new List<TarEntryExceptions>();

            if (String.IsNullOrEmpty(tarEntry.FileName))
            {
                exceptions.Add(TarEntryExceptions.FileNameEmptyOrNull);
            }

            if (!tarEntry.FileType.Equals(FileType.Null)
                && String.IsNullOrEmpty(tarEntry.NameOfLinkedFile))
            {
                exceptions.Add(TarEntryExceptions.MissingFileNameForLinkIndicator);
            }

            if (!tarEntry.FileType.Equals(FileType.Null) && tarEntry.Length != 0)
            {
                exceptions.Add(TarEntryExceptions.LinkedFileContainsData);
            }

            return exceptions;
        }

        private static FileTypeForFileMode GetFileTypeForFileMode(byte value, byte value2)
        {
            if(value == 49)
            {
                byte t = value2;
                if (t == 48)
                {
                    return FileTypeForFileMode.Regular_File;
                }
                else if (t == 50)
                {
                    return FileTypeForFileMode.Symbolic_Link;
                }
                else if (t == 52)
                {
                    return FileTypeForFileMode.Socket;
                }
            }
            else if(value == 32)
            {
                byte t = value2;
                if (t == 49)
                {
                    return FileTypeForFileMode.Fifo;
                }
                else if (t == 50)
                {
                    return FileTypeForFileMode.Character_Device;
                }
                else if (t == 52)
                {
                    return FileTypeForFileMode.Directory;
                }
                else if (t == 54)
                {
                    return FileTypeForFileMode.Block_Device;
                }
            }

            return FileTypeForFileMode.Null;

         //   throw new IOException("ARC-000-007: Unrecognized File Type: Values = " 
          //      + value + ":" + value2);
        }

        private static SpecialAccessRights GetSpecialAccessRightsFor(byte value)
        {
            return SpecialAccessRights.All_Cleared;
        }

        private static AccessRights GetAccessRightsFor(byte value)
        {
            switch (value)
            {
                case 48:
                    return AccessRights.All_Denied;
                case 49:
                    return AccessRights.Execute;
                case 50:
                    return AccessRights.Write;
                case 51:
                    return AccessRights.Write_Execute;
                case 52:
                    return AccessRights.Read;
                case 53:
                    return AccessRights.Read_Execute;
                case 54:
                    return AccessRights.Read_Write;
                case 55:
                    return AccessRights.Read_Write_Execute;
                default:
                    throw new IOException("ARC-000-008: Unrecognized file access: Value = " + value);
            }
        }

        private static FileType GetFileTypeFor(byte value)
        {
            switch (value)
            {
                case 48:
                    return FileType.Normal_File;
                case 49:
                    return FileType.Hard_Link;
                case 50:
                    return FileType.Symbolic_Link;
                case 51:
                    return FileType.Character_Special;
                case 52:
                    return FileType.Block_Special;
                case 53:
                    return FileType.Directory;
                case 54:
                    return FileType.Fifo;
                case 55:
                    return FileType.Contiguous_File;
                case 76:
                    return FileType.Long_Link;
                default:
                    Console.Error.WriteLine("Unknown File Type");
                    return FileType.Normal_File;//according to spec
            }
        }

        private static Boolean IsAscii(byte[] value)
        {
            for (int i = 0; i < value.Length; i++)
            {
                if (value[i] < 0 || value[i] > 127)
                {
                    return false;
                }
            }
            return true;
        }

        private static Boolean IsNumber(byte[] value)
        {
            for (int i = 0; i < value.Length; i++)
            {
                int x = value[i];
                if (x == 32)
                {
                    continue;
                }
                if (x < 48 || x > 57)
                {
                    return false;
                }
            }
            return true;
        }
        #endregion
    }
}
